using System;
using System.Runtime.InteropServices;
using Elevation.Common.Services.Helpers;
using log4net;

namespace Elevation.Common.Services
{
	public static class ProcessService
	{
		#region Constants

		private const uint LogonWithProfile = 1;
		private const uint Infinite = 0xFFFFFFFF;
		private const uint WaitFailed = 0xFFFFFFFF;

		#endregion Constants

		#region Extern

		[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool LogonUser(
			string userName, string domain, string password, LogonType logonType, LogonProvider logonProvider, out IntPtr token);

		[DllImport("advapi32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public extern static bool DuplicateTokenEx(
			IntPtr existingToken, TokenAccess desiredAccess, ref SecurityAttributes tokenAttributes,
			SecurityImpersonationLevel impersonationLevel, TokenType tokenType, out IntPtr newToken);

		[DllImport("advapi32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool ImpersonateLoggedOnUser(IntPtr token);

		[DllImport("advapi32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool RevertToSelf();

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool CreateProcess(
			string applicationName, string commandLine, ref SecurityAttributes processAttributes, ref SecurityAttributes threadAttributes,
			[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
			uint creationFlags, IntPtr environment, string currentDirectory,
			ref StartupInfo startupInfo, out ProcessInformation processInformation);

		[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool CreateProcessWithLogonW(
			string username, string domain, string password, uint logonFlags, string applicationName, string commandLine,
			uint creationFlags, IntPtr environment, string currentDirectory,
			ref StartupInfo startupInfo, out ProcessInformation processInformation);

		[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool CreateProcessAsUser(
			IntPtr token, string applicationName, string commandLine,
			ref SecurityAttributes processAttributes, ref SecurityAttributes threadAttributes,
			[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
			uint creationFlags, IntPtr environment, string currentDirectory,
			ref StartupInfo startupInfo, out ProcessInformation processInformation
		);

		[DllImport("kernel32.dll", SetLastError = true)]
		public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);

		[DllImport("kernel32", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		public static extern bool CloseHandle(IntPtr handle);

		#endregion Extern

		#region Fields

		private static readonly ILog log = LogManager.GetLogger(typeof(ProcessService));

		#endregion Fields

		#region Properties

		private static bool HasUac
		{
			get { return Environment.OSVersion.Version.Major >= 6; }
		}

		#endregion Proprties

		#region Methods

		public static void StartProcess(string commandLine)
		{
			var processSecurity = new SecurityAttributes();
			var threadSecurity = new SecurityAttributes();
			var processInformation = new ProcessInformation();
			var startupInfo = new StartupInfo { Cb = Marshal.SizeOf(typeof(StartupInfo)) };

			try
			{
				var result = CreateProcess(
					null, commandLine, ref processSecurity, ref threadSecurity, false,
					0, IntPtr.Zero, null, ref startupInfo, out processInformation);
				if (!result)
				{
					throw new ApplicationException(string.Format("Failed to create process. Error {0}", Marshal.GetLastWin32Error()));
				}

				var waitResult = WaitForSingleObject(processInformation.Process, Infinite);
				if (waitResult == WaitFailed)
				{
					throw new ApplicationException(string.Format("Waiting for created process wailed. Error {0}", Marshal.GetLastWin32Error()));
				}
			}
			finally
			{
				if (processInformation.Thread != IntPtr.Zero)
				{
					CloseHandle(processInformation.Thread);
				}
				if (processInformation.Process != IntPtr.Zero)
				{
					CloseHandle(processInformation.Process);
				}
			}
		}

		public static void StartProcess(string commandLine, string username, string password)
		{
			if (HasUac)
			{
				log.Info("UAC was detected");
				StartProcessAsUserWithUac(username, password, commandLine);
			}
			else
			{
				log.Info("No UAC was detected");
				StartProcessAsUser(username, password, commandLine);
			}
		}

		#endregion Methods

		#region Helpers

		private static void StartProcessAsUser(string username, string password, string commandLine)
		{
			var processInformation = new ProcessInformation();
			var startupInfo = new StartupInfo { Cb = Marshal.SizeOf(typeof(StartupInfo)) };

			try
			{
				var result = CreateProcessWithLogonW(
					username, null, password, LogonWithProfile, null, commandLine, 0, IntPtr.Zero, null,
					ref startupInfo, out processInformation);
				if (!result)
				{
					throw new ApplicationException(string.Format("Failed to create process. Error {0}", Marshal.GetLastWin32Error()));
				}

				var waitResult = WaitForSingleObject(processInformation.Process, Infinite);
				if (waitResult == WaitFailed)
				{
					throw new ApplicationException(string.Format("Waiting for created process wailed. Error {0}", Marshal.GetLastWin32Error()));
				}
			}
			finally
			{
				if (processInformation.Thread != IntPtr.Zero)
				{
					CloseHandle(processInformation.Thread);
				}
				if (processInformation.Process != IntPtr.Zero)
				{
					CloseHandle(processInformation.Process);
				}
			}
		}

		private static void StartProcessAsUserWithUac(string username, string password, string commandLine)
		{
			log.Info("Calling LogonUser()");
			IntPtr userToken;
			var logonResult = LogonUser(
				username, null, password, LogonType.Logon32LogonInteractive, LogonProvider.Logon32ProviderDefault, out userToken);
			if (!logonResult)
			{
				throw new ApplicationException(string.Format("Failed to logon user. Error: {0}", Marshal.GetLastWin32Error()));
			}
			log.InfoFormat("Got token {0}", userToken);

			try
			{
				log.Info("Calling DuplicateTokenEx()");
				var securityAttributes = new SecurityAttributes {Length = Marshal.SizeOf(typeof(SecurityAttributes))};
				IntPtr duplicatedToken;
				var duplicateResult = DuplicateTokenEx(
					userToken, TokenAccess.TokenAllAccess, ref securityAttributes, SecurityImpersonationLevel.SecurityImpersonation,
					TokenType.TokenPrimary, out duplicatedToken);
				if (!duplicateResult)
				{
					throw new ApplicationException(string.Format("Failed to duplicate user token. Error: {0}", Marshal.GetLastWin32Error()));
				}
				log.InfoFormat("Got duplicated token {0}", duplicatedToken);

				log.Info("Calling ImpersonateLoggedOnUser()");
				var impersonateResult = ImpersonateLoggedOnUser(duplicatedToken);
				if (!impersonateResult)
				{
					throw new ApplicationException(string.Format("Failed to impersonate user. Error: {0}", Marshal.GetLastWin32Error()));
				}

				StartProcess(commandLine);

				log.Info("Calling RevertToSelf()");
				var revertResult = RevertToSelf();
				if (!revertResult)
				{
					throw new ApplicationException(string.Format("Failed to revert impersonation user. Error: {0}", Marshal.GetLastWin32Error()));
				}

				//var processSecurity = new SecurityAttributes();
				//var threadSecurity = new SecurityAttributes();
				//var processInformation = new ProcessInformation();
				//var startupInfo = new StartupInfo { Cb = Marshal.SizeOf(typeof(StartupInfo)) };

				//try
				//{
				//  var createProcessResult = CreateProcessAsUser(
				//    duplicatedToken, null, commandLine, ref processSecurity, ref threadSecurity, false, 0, IntPtr.Zero, null,
				//    ref startupInfo, out processInformation);
				//  if (!createProcessResult)
				//  {
				//    throw new ApplicationException(string.Format("Failed to create process. Error: {0}", Marshal.GetLastWin32Error()));
				//  }

				//  var waitResult = WaitForSingleObject(processInformation.Process, Infinite);
				//  if (waitResult == WaitFailed)
				//  {
				//    throw new ApplicationException(string.Format("Waiting for created process wailed. Error {0}", Marshal.GetLastWin32Error()));
				//  }
				//}
				//finally
				//{
				//  if (processInformation.Thread != IntPtr.Zero)
				//  {
				//    CloseHandle(processInformation.Thread);
				//  }
				//  if (processInformation.Process != IntPtr.Zero)
				//  {
				//    CloseHandle(processInformation.Process);
				//  }
				//}
			}
			finally
			{
				CloseHandle(userToken);
			}
		}

		#endregion Helpers
	}
}